/*
 * Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
 * Version 1.0, and under the Eclipse Public License, Version 1.0
 * (http://h2database.com/html/license.html).
 * Initial Developer: H2 Group
 */
package org.h2.server.web;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Iterator;
import java.util.Locale;
import java.util.Properties;
import java.util.StringTokenizer;

import org.h2.constant.SysProperties;
import org.h2.engine.Constants;
import org.h2.message.TraceSystem;
import org.h2.util.IOUtils;
import org.h2.util.NetUtils;
import org.h2.util.StringUtils;
import org.h2.util.Utils;

/**
 * For each connection to a session, an object of this class is created.
 * This class is used by the H2 Console.
 */
class WebThread extends WebApp implements Runnable {

	protected OutputStream output;
	protected Socket socket;
	private Thread thread;
	private InputStream input;
	private int headerBytes;
	private String ifModifiedSince;

	WebThread(Socket socket, WebServer server) {
		super(server);
		this.socket = socket;
		thread = new Thread(this, "H2 Console thread");
	}

	/**
	 * Start the thread.
	 */
	void start() {
		thread.start();
	}

	/**
	 * Wait until the thread is stopped.
	 *
	 * @param millis the maximum number of milliseconds to wait
	 */
	void join(int millis) throws InterruptedException {
		thread.join(millis);
	}

	/**
	 * Close the connection now.
	 */
	void stopNow() {
		this.stop = true;
		try {
			socket.close();
		} catch (IOException e) {
			// ignore
		}
	}

	private String getAllowedFile(String requestedFile) {
		if (!allow()) {
			return "notAllowed.jsp";
		}
		if (requestedFile.length() == 0) {
			return "index.do";
		}
		return requestedFile;
	}

	public void run() {
		try {
			input = new BufferedInputStream(socket.getInputStream());
			output = new BufferedOutputStream(socket.getOutputStream());
			while (!stop) {
				if (!process()) {
					break;
				}
			}
		} catch (Exception e) {
			TraceSystem.traceThrowable(e);
		}
		IOUtils.closeSilently(output);
		IOUtils.closeSilently(input);
		try {
			socket.close();
		} catch (IOException e) {
			// ignore
		} finally {
			server.remove(this);
		}
	}

	@SuppressWarnings("unchecked")
	private boolean process() throws IOException {
		boolean keepAlive = false;
		String head = readHeaderLine();
		if (head.startsWith("GET ") || head.startsWith("POST ")) {
			int begin = head.indexOf('/'), end = head.lastIndexOf(' ');
			String file;
			if (begin < 0 || end < begin) {
				file = "";
			} else {
				file = head.substring(begin + 1, end).trim();
			}
			trace(head + ": " + file);
			file = getAllowedFile(file);
			attributes = new Properties();
			int paramIndex = file.indexOf("?");
			session = null;
			if (paramIndex >= 0) {
				String attrib = file.substring(paramIndex + 1);
				parseAttributes(attrib);
				String sessionId = attributes.getProperty("jsessionid");
				file = file.substring(0, paramIndex);
				session = server.getSession(sessionId);
			}
			keepAlive = parseHeader();
			String hostAddr = socket.getInetAddress().getHostAddress();
			file = processRequest(file, hostAddr);
			if (file.length() == 0) {
				// asynchronous request
				return true;
			}
			String message;
			byte[] bytes;
			if (cache && ifModifiedSince != null && ifModifiedSince.equals(server.getStartDateTime())) {
				bytes = null;
				message = "HTTP/1.1 304 Not Modified\r\n";
			} else {
				bytes = server.getFile(file);
				if (bytes == null) {
					message = "HTTP/1.0 404 Not Found\r\n";
					bytes = StringUtils.utf8Encode("File not found: " + file);
				} else {
					if (session != null && file.endsWith(".jsp")) {
						String page = StringUtils.utf8Decode(bytes);
						if (SysProperties.CONSOLE_STREAM) {
							Iterator<String> it = (Iterator<String>) session.map.remove("chunks");
							if (it != null) {
								message = "HTTP/1.1 200 OK\r\n";
								message += "Content-Type: " + mimeType + "\r\n";
								message += "Cache-Control: no-cache\r\n";
								message += "Transfer-Encoding: chunked\r\n";
								message += "\r\n";
								trace(message);
								output.write(message.getBytes());
								while (it.hasNext()) {
									String s = it.next();
									s = PageParser.parse(s, session.map);
									bytes = StringUtils.utf8Encode(s);
									if (bytes.length == 0) {
										continue;
									}
									output.write(Integer.toHexString(bytes.length).getBytes());
									output.write("\r\n".getBytes());
									output.write(bytes);
									output.write("\r\n".getBytes());
									output.flush();
								}
								output.write("0\r\n\r\n".getBytes());
								output.flush();
								return keepAlive;
							}
						}
						page = PageParser.parse(page, session.map);
						bytes = StringUtils.utf8Encode(page);
					}
					message = "HTTP/1.1 200 OK\r\n";
					message += "Content-Type: " + mimeType + "\r\n";
					if (!cache) {
						message += "Cache-Control: no-cache\r\n";
					} else {
						message += "Cache-Control: max-age=10\r\n";
						message += "Last-Modified: " + server.getStartDateTime() + "\r\n";
					}
					message += "Content-Length: " + bytes.length + "\r\n";
				}
			}
			message += "\r\n";
			trace(message);
			output.write(message.getBytes());
			if (bytes != null) {
				output.write(bytes);
			}
			output.flush();
		}
		return keepAlive;
	}

	private String readHeaderLine() throws IOException {
		StringBuilder buff = new StringBuilder();
		while (true) {
			headerBytes++;
			int c = input.read();
			if (c == -1) {
				throw new IOException("Unexpected EOF");
			} else if (c == '\r') {
				headerBytes++;
				if (input.read() == '\n') {
					return buff.length() > 0 ? buff.toString() : null;
				}
			} else if (c == '\n') {
				return buff.length() > 0 ? buff.toString() : null;
			} else {
				buff.append((char) c);
			}
		}
	}

	private void parseAttributes(String s) {
		trace("data=" + s);
		while (s != null) {
			int idx = s.indexOf('=');
			if (idx >= 0) {
				String property = s.substring(0, idx);
				s = s.substring(idx + 1);
				idx = s.indexOf('&');
				String value;
				if (idx >= 0) {
					value = s.substring(0, idx);
					s = s.substring(idx + 1);
				} else {
					value = s;
				}
				String attr = StringUtils.urlDecode(value);
				attributes.put(property, attr);
			} else {
				break;
			}
		}
		trace(attributes.toString());
	}

	private boolean parseHeader() throws IOException {
		boolean keepAlive = false;
		trace("parseHeader");
		int len = 0;
		ifModifiedSince = null;
		boolean multipart = false;
		while (true) {
			String line = readHeaderLine();
			if (line == null) {
				break;
			}
			trace(" " + line);
			String lower = StringUtils.toLowerEnglish(line);
			if (lower.startsWith("if-modified-since")) {
				ifModifiedSince = getHeaderLineValue(line);
			} else if (lower.startsWith("connection")) {
				String conn = getHeaderLineValue(line);
				if ("keep-alive".equals(conn)) {
					keepAlive = true;
				}
			} else if (lower.startsWith("content-type")) {
				String type = getHeaderLineValue(line);
				if (type.startsWith("multipart/form-data")) {
					multipart = true;
				}
			} else if (lower.startsWith("content-length")) {
				len = Integer.parseInt(getHeaderLineValue(line));
				trace("len=" + len);
			} else if (lower.startsWith("user-agent")) {
				boolean isWebKit = lower.indexOf("webkit/") >= 0;
				if (isWebKit && session != null) {
					// workaround for what seems to be a WebKit bug:
					// http://code.google.com/p/chromium/issues/detail?id=6402
					session.put("frame-border", "1");
					session.put("frameset-border", "2");
				}
			} else if (lower.startsWith("accept-language")) {
				Locale locale = session == null ? null : session.locale;
				if (locale == null) {
					String languages = getHeaderLineValue(line);
					StringTokenizer tokenizer = new StringTokenizer(languages, ",;");
					while (tokenizer.hasMoreTokens()) {
						String token = tokenizer.nextToken();
						if (!token.startsWith("q=")) {
							if (server.supportsLanguage(token)) {
								int dash = token.indexOf('-');
								if (dash >= 0) {
									String language = token.substring(0, dash);
									String country = token.substring(dash + 1);
									locale = new Locale(language, country);
								} else {
									locale = new Locale(token, "");
								}
								headerLanguage = locale.getLanguage();
								if (session != null) {
									session.locale = locale;
									session.put("language", headerLanguage);
									server.readTranslations(session, headerLanguage);
								}
								break;
							}
						}
					}
				}
			} else if (line.trim().length() == 0) {
				break;
			}
		}
		if (multipart) {
			uploadMultipart(input, len);
		} else if (session != null && len > 0) {
			byte[] bytes = Utils.newBytes(len);
			for (int pos = 0; pos < len;) {
				pos += input.read(bytes, pos, len - pos);
			}
			String s = new String(bytes);
			parseAttributes(s);
		}
		return keepAlive;
	}

	private void uploadMultipart(InputStream in, int len) throws IOException {
		if (!new File(WebServer.TRANSFER).exists()) {
			return;
		}
		String fileName = "temp.bin";
		headerBytes = 0;
		String boundary = readHeaderLine();
		while (true) {
			String line = readHeaderLine();
			if (line == null) {
				break;
			}
			int index = line.indexOf("filename=\"");
			if (index > 0) {
				fileName = line.substring(index + "filename=\"".length(), line.lastIndexOf('"'));
			}
			trace(" " + line);
		}
		if (!WebServer.isSimpleName(fileName)) {
			return;
		}
		len -= headerBytes;
		File file = new File(WebServer.TRANSFER, fileName);
		OutputStream out = new FileOutputStream(file);
		IOUtils.copy(in, out, len);
		out.close();
		// remove the boundary
		RandomAccessFile f = new RandomAccessFile(file, "rw");
		int testSize = (int) Math.min(f.length(), Constants.IO_BUFFER_SIZE);
		f.seek(f.length() - testSize);
		byte[] bytes = Utils.newBytes(Constants.IO_BUFFER_SIZE);
		f.readFully(bytes, 0, testSize);
		String s = new String(bytes, "ASCII");
		int x = s.lastIndexOf(boundary);
		f.setLength(f.length() - testSize + x - 2);
		f.close();
	}

	private static String getHeaderLineValue(String line) {
		return line.substring(line.indexOf(':') + 1).trim();
	}

	protected String adminShutdown() {
		stopNow();
		return super.adminShutdown();
	}

	private boolean allow() {
		if (server.getAllowOthers()) {
			return true;
		}
		try {
			return NetUtils.isLocalAddress(socket);
		} catch (UnknownHostException e) {
			server.traceError(e);
			return false;
		}
	}

	private void trace(String s) {
		server.trace(s);
	}
}
